// --------------------------------------------------------------------------------------------------------------------
// <copyright file="SimpleModbusTCPConnection.cs" company="Microsoft Corporation">
// The MIT License (MIT)
// 
// Copyright (c) 2014, Microsoft Corporation
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace Microsoft.Robotics.Hardware.Modbus
{
    using System;
    using System.Net.Sockets;
    using System.Runtime.Serialization;

    /// <summary>
    /// A partial implementation of ModbusTCP http://en.wikipedia.org/wiki/Modbus implementing 
    /// function codes 4 (Read Input Registers) and 16 (Write Multiple Registers) for a single 
    /// remote device.
    /// </summary>
    public class SimpleModbusTCPConnection : IDisposable, ISimpleModbusTCPConnection
    {
        /// <summary>
        /// Well known Modbus port
        /// </summary>
        private const int DefaultModbusPort = 502;

        /// <summary>
        ///  Initial size of packet buffer.
        /// </summary>
        private const int DefaultBufferSize = 256;

        /// <summary>
        /// TCP client used to connect to remote device
        /// </summary>
        private TcpClient client;

        /// <summary>
        /// Stream used to read/write to remote device
        /// </summary>
        private NetworkStream stream;

        /// <summary>
        /// Buffer used internally to build packets
        /// </summary>
        private byte[] packetBuffer;

        /// <summary>
        /// Used to generate random numbers
        /// </summary>
        private Random randomNumGen;

        /// <summary>
        /// Track if we have been disposed or not
        /// </summary>
        private bool disposed = false;

        /// <summary>
        /// Initializes a new instance of the <see cref="SimpleModbusTCPConnection"/> class.
        /// </summary>
        /// <param name="ipAddress">IP address to connect to.</param>
        public SimpleModbusTCPConnection(string ipAddress)
        {
            this.packetBuffer = new byte[DefaultBufferSize];
            this.client = new TcpClient(ipAddress, DefaultModbusPort);
            this.stream = this.client.GetStream();
            this.randomNumGen = new Random(0);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="SimpleModbusTCPConnection"/> class without attempting to open
        /// an TCP connection.
        /// </summary>
        protected SimpleModbusTCPConnection()
        {
            this.packetBuffer = new byte[DefaultBufferSize];
            this.client = null;
            this.stream = null;
            this.randomNumGen = new Random(0);
        }

        /// <summary>
        /// Dispose of unmanaged resources
        /// </summary>
        public void Dispose()
        {
            // follow standard dispose pattern
            this.Dispose(true);

            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Creates a ReadInputRegisters request, sends it to the slave device, 
        /// reads the response, and places them into the out parameter. 
        /// </summary>
        /// <param name="unitID">ID of recipient.</param>
        /// <param name="addressOfFirstRegister">Address of the first register to read.</param>
        /// <param name="wordsToReadCount">Number of 16-bit words to read.</param>
        /// <param name="registers">Returns the registers read in response.</param>
        public void ReadInputRegisters(
            byte unitID,
            ushort addressOfFirstRegister,
            ushort wordsToReadCount,
            out ushort[] registers)
        {
            // ensures that no other transactions will take palce concurrently with this one
            // transaction ID is chosen random and used to confirm that correct response is read
            byte[] transactionID = new byte[ModbusTCPPacket.TransactionIDByteCount];
            this.randomNumGen.NextBytes(transactionID);

            int packetSize = ReadInputRegistersPacket.BuildReadInputRegistersRequest(
                unitID,
                addressOfFirstRegister,
                wordsToReadCount,
                transactionID,
                this.packetBuffer);

            // Send packet
            this.SendPacket(this.packetBuffer, packetSize);

            // Get Response
            // reuse the this.packetBuffer
            this.ReadPacket(this.packetBuffer);

            // TODO:  Better error handling
            // Parse response
            ReadInputRegistersPacket.ParseReadInputRegistersResponse(transactionID, this.packetBuffer, out registers);
        }

        /// <summary>
        /// Set multiple registers.
        /// </summary>
        /// <param name="unitID">Slave device ID.</param>
        /// <param name="addressOfFirstRegister">Address of register to begin writing at.</param>
        /// <param name="wordsToWriteCount">Number of words to write.</param>
        /// <param name="registers">Values to write.</param>
        public void PresetMultipleRegisters(
            byte unitID,
            ushort addressOfFirstRegister,
            ushort wordsToWriteCount,
            ushort[] registers)
        {
            // ensures that no other transactions will take palce concurrently with this one
            // transaction ID is chosen random and used to confirm that correct response is read
            byte[] transactionIDBytes = new byte[ModbusTCPPacket.TransactionIDByteCount];
            this.randomNumGen.NextBytes(transactionIDBytes);

            int packetSize = PresetMultipleRegistersPacket.BuildPresetMultipleRegistersRequest(
                unitID,
                addressOfFirstRegister,
                wordsToWriteCount,
                transactionIDBytes,
                registers,
                this.packetBuffer);

            // Send packet
            this.SendPacket(this.packetBuffer, packetSize);

            // TODO:  Better error handling
            // Get Response
            // reuse the this.packetBuffer
            this.ReadPacket(this.packetBuffer);
        }

        /// <summary>
        /// Dispose of unmanaged resources.
        /// </summary>
        /// <param name="disposing">True if called by a user's code.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    if (this.client != null)
                    {
                        this.client.Close();
                    }

                    if (this.stream != null)
                    {
                        this.stream.Dispose();
                    }
                }
            }
        }

        /// <summary>
        /// Send a packet.
        /// </summary>
        /// <param name="packet">Packet to send.</param>
        /// <param name="bytesToWrite">Number of bytes to be written.</param>
        protected virtual void SendPacket(byte[] packet, int bytesToWrite)
        {
            if (packet == null)
            {
                throw new ArgumentException("packet cannot be null");
            }

            if (packet.Length < bytesToWrite)
            {
                throw new ArgumentException("length of packet is less than the number of bytes to be written");
            }

            this.stream.Write(packet, 0, bytesToWrite);
        }

        /// <summary>
        /// Read a packet
        /// </summary>
        /// <param name="readBuffer">Buffer to store packet in.</param>
        /// <returns>The number of bytes read.</returns>
        protected virtual int ReadPacket(byte[] readBuffer)
        {
            int bytesRead = 0;

            while (!ModbusTCPPacket.IsCompletePacket(readBuffer, bytesRead))
            {
                bytesRead += this.stream.Read(readBuffer, 0, readBuffer.Length);
            }

            return bytesRead;
        }
    }
}
